//Copyright     2011 - Jonathan Allen

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;

#if ATTRIBUTE_VALIDATION
using System.ComponentModel.DataAnnotations;
#endif

namespace Granite.Metadata
{
    /// <summary>
    /// This is a cache of metadata about a specific property.
    /// </summary>
    public partial class PropertyMetadata
    {
        readonly List<PropertyMetadata> m_CalculatedFields = new List<PropertyMetadata>();

        //readonly Dictionary<string, string> m_ActionMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        readonly string m_Name;
        readonly MethodInfo m_GetMethod;
        readonly bool m_IsIndexed;
        readonly PropertyInfo m_PropertyInfo;
        readonly MethodInfo m_SetMethod;
        readonly Type m_PropertyType;
        readonly PropertyChangedEventArgs m_Args;

        [Pure]
        internal PropertyMetadata(PropertyInfo info)
        {
            Contract.Requires(info != null);

            m_PropertyInfo = info;

            //foreach (ActionAttribute action in info.GetCustomAttributes(typeof(ActionAttribute), true))
            //	m_ActionMap[action.ActionName] = action.ParameterName ?? info.Name;

            CacheValidators(info);

            m_IsIndexed = info.GetIndexParameters().Length > 0;

            m_GetMethod = m_PropertyInfo.GetGetMethod(true);
            m_SetMethod = m_PropertyInfo.GetSetMethod(true);

            m_PropertyType = info.PropertyType;

            var name = info.ToString();
            m_Name = name.Substring(name.IndexOf(" ", StringComparison.Ordinal) + 1);

            //we create this now so that it will be physically close to the dictionary that contains the property metadata.
            if (IsIndexed)
                m_Args = new PropertyChangedEventArgs(info.Name + "[]");
            else
                m_Args = new PropertyChangedEventArgs(info.Name);
        }

        /// <summary>
        /// Returns true of this property needs to trigger updates to calculated fields
        /// </summary>
        public bool AffectsCalculatedFields
        {
            get { return m_CalculatedFields.Count > 0; }
        }

        /// <summary>
        /// This returns a list of calculated fields that need to be updated when this property is changed.
        /// </summary>
        public ReadOnlyCollection<PropertyMetadata> CalculatedFields
        {
            get
            {
                Contract.Ensures(Contract.Result<ReadOnlyCollection<PropertyMetadata>>() != null);

                return new ReadOnlyCollection<PropertyMetadata>(m_CalculatedFields);
            }
        }

        ///// <summary>
        ///// This is a list of service/stored procedure calls that use this property.
        ///// </summary>
        //public Dictionary<string, string> ActionMap
        //{
        //	//TODO: Replace this with a read-only dictionary
        //	get { return m_ActionMap; }
        //}

        /// <summary>
        /// Returns true if there is a public getter
        /// </summary>
        public bool CanRead
        {
            get
            {
                Contract.Ensures(Contract.Result<bool>() == false || m_GetMethod != null);
                return m_GetMethod != null && m_GetMethod.IsPublic && !m_IsIndexed;
            }
        }

        /// <summary>
        /// Returns true is there is a public setter
        /// </summary>
        public bool CanWrite
        {
            get
            {
                Contract.Ensures(Contract.Result<bool>() == false || m_SetMethod != null);
                return m_SetMethod != null && m_SetMethod.IsPublic && !m_IsIndexed;
            }
        }

        /// <summary>
        /// Public name of the property
        /// </summary>
        public string Name
        {
            get
            {
                Contract.Ensures(!String.IsNullOrEmpty(Contract.Result<string>()));

                return m_Name;
            }
        }

        /// <summary>
        /// Cached PropertyInfo for the property.
        /// </summary>
        internal PropertyInfo PropertyInfo
        {
            get
            {
                Contract.Ensures(Contract.Result<PropertyInfo>() != null);

                return m_PropertyInfo;
            }
        }

        /// <summary>
        /// Invokes this property's getter on the supplied object
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "CanRead")]
        public object InvokeGet(object target)
        {
            Contract.Requires(CanRead);

            if (CanRead)
                return m_GetMethod.Invoke(target, null);
            else
                throw new InvalidOperationException("CanRead is false.");
        }

        /// <summary>
        /// Invokes this property's setter on the supplied object
        /// </summary>
        /// <param name="target"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "CanWrite")]
        public object InvokeSet(object target, object value)
        {
            Contract.Requires(CanWrite);

            if (CanWrite)
                return m_SetMethod.Invoke(target, new object[] { value });
            else
                throw new InvalidOperationException("CanWrite is false");
        }

        /// <summary>
        /// Adds a property to the list of calculated values watching this property.
        /// </summary>
        /// <param name="affectedProperty"></param>
        internal void AddCalculatedField(PropertyMetadata affectedProperty)
        {
            Contract.Requires(affectedProperty != null);
            //Contract.Requires(affectedProperty.CanRead);

            m_CalculatedFields.Add(affectedProperty);
        }

        partial void CacheValidators(PropertyInfo info);

        /// <summary>
        /// Gets the type of this property.
        /// </summary>
        public Type PropertyType
        {
            get
            {
                Contract.Ensures(Contract.Result<Type>() != null);

                return m_PropertyType;
            }
        }

        /// <summary>
        /// Gets a cached instance of PropertyChangedEventArgs
        /// </summary>
        /// <remarks>For indexed properties such as "Item [Int32]" the property name will be reduced to "Item[]" to match ObservableCollection.</remarks>
        public PropertyChangedEventArgs PropertyChangedEventArgs
        {
            get
            {
                Contract.Ensures(Contract.Result<PropertyChangedEventArgs>() != null);

                return m_Args;
            }
        }

        [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic"), SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        [ContractInvariantMethod]
        void ObjectInvariant()
        {
            Contract.Invariant(m_PropertyInfo != null);
            Contract.Invariant(m_CalculatedFields != null);
            Contract.Invariant(m_PropertyType != null);
            Contract.Invariant(m_Args != null);
            Contract.Invariant(!String.IsNullOrEmpty(m_Name));

#if ATTRIBUTE_VALIDATION
            Contract.Invariant(m_Validators != null);

#endif

        }

        /// <summary>
        /// Returns true if this represents an indexed property
        /// </summary>
        public bool IsIndexed
        {
            get { return m_IsIndexed; }
        }
    }

#if ATTRIBUTE_VALIDATION

    public partial class PropertyMetadata
    {
        ReadOnlyCollection<ValidationAttribute> m_Validators;

        /// <summary>
        /// List of validators that apply to the property
        /// </summary>
        public ReadOnlyCollection<ValidationAttribute> Validators
        {
            get
            {
                Contract.Ensures(Contract.Result<ReadOnlyCollection<ValidationAttribute>>() != null);
                return m_Validators;
            }
        }

        partial void CacheValidators(PropertyInfo info)
        {
            Contract.Requires(info != null);

            var validators = new List<ValidationAttribute>();
            foreach (ValidationAttribute attribute in info.GetCustomAttributes(typeof(ValidationAttribute), true))
                validators.Add(attribute);
            m_Validators = new ReadOnlyCollection<ValidationAttribute>(validators);
        }

    }

#endif

}
